/**
 * 
 */
package metagrammar;

import static com.google.common.base.Preconditions.checkArgument;

import java.util.HashSet;
import java.util.Set;
import java.util.UUID;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;

import metagrammar.Operator.Instance;

import com.google.common.collect.ImmutableList;


/**
 * This class represents a transformation from syntax tree A to syntax tree B
 * where both A and B are represented by an element of {@link Operator.Instance}.
 * 
 * @author Joshua Lockerman
 *
 */
@XmlAccessorType(XmlAccessType.FIELD)
public class Transform {
    
    public static class Wildcard extends Operator.Instance {
        public Wildcard(Type type)
        {
            (new Operator(type)).super();
        }
        
        public Wildcard(Operator op)
        {
            op.super();
        }
        
        @Override
        public boolean canTransformTo(Operator.Instance expression)
        {
            return true;
        }
        
        @Override
        public Operator.Instance copy()
        {
            return null;
        }
    }
    
    public static class Variable extends Operator.Instance {
        
        private final UUID id;
        private int index;
        
        public Variable(Type type)
        {
            (new Operator(type)).super();
            id = UUID.randomUUID();
        }
        
        public Variable(Operator op)
        {
            op.super();
            id = UUID.randomUUID();
        }
        
        private int getIndex()
        {
            return index;
        }
        
        private void setIndex(int id)
        {
            this.index = id;
        }
        
        /* (non-Javadoc)
         * @see java.lang.Object#hashCode()
         */
        @Override
        public int hashCode()
        {
            return id.hashCode();
        }
        
        /* (non-Javadoc)
         * @see java.lang.Object#equals(java.lang.Object)
         */
        @Override
        public boolean equals(Object obj)
        {
            if(this == obj) {
                return true;
            }
            if(!super.equals(obj)) {
                return false;
            }
            if(!(obj instanceof Variable)) {
                return false;
            }
            Variable other = (Variable) obj;
            if(id == null) {
                if(other.id != null) {
                    return false;
                }
            }
            else if(!id.equals(other.id)) {
                return false;
            }
            return true;
        }
        
        @Override
        public Operator.Instance copy()
        {
            return null;
        }
        
        
        
    }
    
    private final Operator.Instance input;
    private final Operator.Instance output;
    
    private final int numberOfVariables;
    
    private Transform()
    {
        input = null;
        output = null;
        numberOfVariables = 0;
    }
    
    public Transform(Operator.Instance input, Operator.Instance output)
    {
        this.input = input;
        this.output = output;
        Set<Variable> foundVars = new HashSet<Variable>();
        int inVars = varcount(input, 0, foundVars);
        checkArgument(inVars == varcount(output, 0, new HashSet<Variable>()));
        numberOfVariables = inVars;
    }
    
    private int varcount(Operator.Instance instance, int index, Set<Variable> foundVars)
    {
        int vars = 0;
        ImmutableList<Operator.Instance> inputTuple = instance.getInputTuple();
        for(int i = 0; i < inputTuple.size(); i++) {
            Operator.Instance input = inputTuple.get(i);
            if(input instanceof Variable && foundVars.add((Variable) input)) {
                vars++;
                ((Variable) input).setIndex(index);
                index++;
            }
            vars += varcount(input, index, foundVars);
        }
        return vars;
    }
    
    
    /**
     * Checks if an input-output pair is a valid set of input
     * and output for this transformation.
     * 
     * @param input
     * @param output
     * @return
     */
    public boolean isValid(Operator.Instance input, Operator.Instance output)
    {
        if(!this.input.getOperator().equals(input.getOperator())) {
            return false;
        }
        if(!this.output.getOperator().equals(output.getOperator())) {
            return false;
        }
        ImmutableList<Operator.Instance> inputTuple = input.getInputTuple();
        ImmutableList<Operator.Instance> requiredInputTuple = this.input.getInputTuple();
        Operator.Instance[] variables = new Operator.Instance[this.numberOfVariables];
        for(int i = 0; i < inputTuple.size(); i++) {
            Operator.Instance required = requiredInputTuple.get(i);
            Operator.Instance found = inputTuple.get(i);
            if(!(required instanceof Wildcard) && !found.canTransformTo(required)) {
                return false;
            }
            if(required instanceof Variable) {
                Variable var = (Variable) required;
                variables[var.getIndex()] = found;
            }
        }
        inputTuple = this.output.getInputTuple();
        requiredInputTuple = output.getInputTuple();
        for(int i = 0; i < inputTuple.size(); i++) {
            Operator.Instance found = inputTuple.get(i);
            Operator.Instance required = requiredInputTuple.get(i);
            if(found instanceof Variable) {
                Variable var = (Variable) found;
                Operator.Instance op = variables[var.getIndex()];
                if(!op.canTransformTo(required)) {
                    return false;
                }
            }
            else if(!found.canTransformTo(required)) {
                return false;
            }
        }
        return true;
    }
    
    public boolean isValidInput(Operator.Instance input)
    {
        if(!this.input.getOperator().equals(input.getOperator())) {
            return false;
        }
        ImmutableList<Operator.Instance> inputTuple = input.getInputTuple();
        ImmutableList<Operator.Instance> requiredInputTuple = this.input.getInputTuple();
        for(int i = 0; i < inputTuple.size(); i++) {
            Operator.Instance required = requiredInputTuple.get(i);
            Operator.Instance found = inputTuple.get(i);
            if(!(required instanceof Wildcard) && !found.canTransformTo(required)) {
                return false;
            }
        }
        return true;
    }
    
    public Operator.Instance transform(Operator.Instance operatorInstance)
    {
        ImmutableList<Operator.Instance> inputTuple = operatorInstance.getInputTuple();
        ImmutableList<Operator.Instance> requiredInputTuple = this.input.getInputTuple();
        Operator.Instance[] variables = new Operator.Instance[this.numberOfVariables];
        for(int i = 0; i < inputTuple.size(); i++) {
            Instance found = inputTuple.get(i);
            Operator.Instance required = requiredInputTuple.get(i);
            if(!(required instanceof Wildcard) && found.getOperator() != required.getOperator()) {
                throw new IllegalArgumentException(
                "operatorInstance must be valid input to transform.");
            }
            if(required instanceof Variable) {
                Variable var = (Variable) required;
                variables[var.getIndex()] = found;
            }
        }
        return handleTransform(variables);
    }
    
    /**
     * @param variables
     * @return
     */
    private Operator.Instance handleTransform(Operator.Instance[] variables)
    {
        Operator.Instance transform = output.copy();
        setVariables(transform, variables);
        return transform;
    }
    
    /**
     * @param instance
     * @param requiredInputTuple
     * @param variables
     */
    private void setVariables(Operator.Instance instance,
            Operator.Instance[] variables)
    {
        ImmutableList<Operator.Instance> inputTuple = instance.getInputTuple();
        for(int i = 0; i < inputTuple.size(); i++) {
            Operator.Instance required = inputTuple.get(i);
            if(required instanceof Variable) {
                Variable var = (Variable) required;
                instance.set(i, variables[var.getIndex()]);
            }
            else if(!(required instanceof Wildcard)) {
                setVariables(inputTuple.get(i), variables);
            }
        }
    }
    
    public Operator getInputOperator()
    {
        return input.getOperator();
    }
    
    public Operator getOutputOperator()
    {
        return output.getOperator();
    }
}
